{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "5a03e0f7",
   "metadata": {},
   "source": [
    "# 셀프 쿼리(Self-querying)\n",
    "\n",
    "`SelfQueryRetriever` 는 자체적으로 질문을 생성하고 해결할 수 있는 기능을 갖춘 검색 도구입니다. \n",
    "\n",
    "이는 사용자가 제공한 자연어 질의를 바탕으로, `query-constructing` LLM chain을 사용해 구조화된 질의를 만듭니다. 그 후, 이 구조화된 질의를 기본 벡터 데이터 저장소(VectorStore)에 적용하여 검색을 수행합니다.\n",
    "\n",
    "이 과정을 통해, `SelfQueryRetriever` 는 단순히 사용자의 입력 질의를 저장된 문서의 내용과 의미적으로 비교하는 것을 넘어서, 사용자의 질의에서 문서의 메타데이터에 대한 **필터를 추출** 하고, 이 필터를 실행하여 관련된 문서를 찾을 수 있습니다. \n",
    "\n",
    "[참고]\n",
    "\n",
    "- LangChain 이 지원하는 셀프 쿼리 검색기(Self-query Retriever) 목록은 [여기](https://python.langchain.com/docs/integrations/retrievers/self_query) 에서 확인해 주시기 바랍니다.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "dd5cfe5f",
   "metadata": {},
   "outputs": [],
   "source": [
    "# API 키를 환경변수로 관리하기 위한 설정 파일\n",
    "from dotenv import load_dotenv\n",
    "\n",
    "# API 키 정보 로드\n",
    "load_dotenv()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "a2e8e07c",
   "metadata": {},
   "outputs": [],
   "source": [
    "# LangSmith 추적을 설정합니다. https://smith.langchain.com\n",
    "# !pip install langchain-teddynote\n",
    "from langchain_teddynote import logging\n",
    "\n",
    "# 프로젝트 이름을 입력합니다.\n",
    "logging.langsmith(\"CH10-Retriever\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "468e6696",
   "metadata": {},
   "source": [
    "## 샘플 데이터 생성"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "b96636f8",
   "metadata": {},
   "source": [
    "화장품 상품의 설명과 메타데이터를 기반으로 유사도 검색이 가능한 벡터 저장소를 구축합니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "19f836a1",
   "metadata": {},
   "outputs": [],
   "source": [
    "from langchain_chroma import Chroma\n",
    "from langchain_core.documents import Document\n",
    "from langchain_openai import OpenAIEmbeddings\n",
    "\n",
    "# 화장품 상품의 설명과 메타데이터 생성\n",
    "docs = [\n",
    "    Document(\n",
    "        page_content=\"수분 가득한 히알루론산 세럼으로 피부 속 깊은 곳까지 수분을 공급합니다.\",\n",
    "        metadata={\"year\": 2024, \"category\": \"스킨케어\", \"user_rating\": 4.7},\n",
    "    ),\n",
    "    Document(\n",
    "        page_content=\"24시간 지속되는 매트한 피니시의 파운데이션, 모공을 커버하고 자연스러운 피부 표현이 가능합니다.\",\n",
    "        metadata={\"year\": 2023, \"category\": \"메이크업\", \"user_rating\": 4.5},\n",
    "    ),\n",
    "    Document(\n",
    "        page_content=\"식물성 성분으로 만든 저자극 클렌징 오일, 메이크업과 노폐물을 부드럽게 제거합니다.\",\n",
    "        metadata={\"year\": 2023, \"category\": \"클렌징\", \"user_rating\": 4.8},\n",
    "    ),\n",
    "    Document(\n",
    "        page_content=\"비타민 C 함유 브라이트닝 크림, 칙칙한 피부톤을 환하게 밝혀줍니다.\",\n",
    "        metadata={\"year\": 2023, \"category\": \"스킨케어\", \"user_rating\": 4.6},\n",
    "    ),\n",
    "    Document(\n",
    "        page_content=\"롱래스팅 립스틱, 선명한 발색과 촉촉한 사용감으로 하루종일 편안하게 사용 가능합니다.\",\n",
    "        metadata={\"year\": 2024, \"category\": \"메이크업\", \"user_rating\": 4.4},\n",
    "    ),\n",
    "    Document(\n",
    "        page_content=\"자외선 차단 기능이 있는 톤업 선크림, SPF50+/PA++++ 높은 자외선 차단 지수로 피부를 보호합니다.\",\n",
    "        metadata={\"year\": 2024, \"category\": \"선케어\", \"user_rating\": 4.9},\n",
    "    ),\n",
    "]\n",
    "\n",
    "# 벡터 저장소 생성\n",
    "vectorstore = Chroma.from_documents(\n",
    "    docs, OpenAIEmbeddings(model=\"text-embedding-3-small\")\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "781d49bf",
   "metadata": {},
   "source": [
    "## SelfQueryRetriever\n",
    "\n",
    "이제 retriever를 인스턴스화할 수 있습니다. 이를 위해서는 문서가 지원하는 **메타데이터 필드** 와 문서 내용에 대한 **간단한 설명을 미리 제공** 해야 합니다.\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "3d43f000",
   "metadata": {},
   "source": [
    "`AttributeInfo` 클래스를 사용하여 화장품 메타데이터 필드에 대한 정보를 정의합니다.\n",
    "\n",
    "- 카테고리(`category`): 문자열 타입, 화장품의 카테고리를 나타내며 ['스킨케어', '메이크업', '클렌징', '선케어'] 중 하나의 값을 가집니다.\n",
    "- 연도(`year`): 정수 타입, 화장품이 출시된 연도를 나타냅니다.\n",
    "- 사용자 평점(`user_rating`): 실수 타입, 1-5 범위의 사용자 평점을 나타냅니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "8dd17e37",
   "metadata": {},
   "outputs": [],
   "source": [
    "from langchain.chains.query_constructor.base import AttributeInfo\n",
    "\n",
    "\n",
    "# 메타데이터 필드 정보 생성\n",
    "metadata_field_info = [\n",
    "    AttributeInfo(\n",
    "        name=\"category\",\n",
    "        description=\"The category of the cosmetic product. One of ['스킨케어', '메이크업', '클렌징', '선케어']\",\n",
    "        type=\"string\",\n",
    "    ),\n",
    "    AttributeInfo(\n",
    "        name=\"year\",\n",
    "        description=\"The year the cosmetic product was released\",\n",
    "        type=\"integer\",\n",
    "    ),\n",
    "    AttributeInfo(\n",
    "        name=\"user_rating\",\n",
    "        description=\"A user rating for the cosmetic product, ranging from 1 to 5\",\n",
    "        type=\"float\",\n",
    "    ),\n",
    "]"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "00be94e2",
   "metadata": {},
   "source": [
    "`SelfQueryRetriever.from_llm()` 메서드를 사용하여 `retriever` 객체를 생성합니다.\n",
    "\n",
    "- `llm`: 언어 모델\n",
    "- `vectorstore`: 벡터 저장소\n",
    "- `document_contents`: 문서들의 내용 설명\n",
    "- `metadata_field_info`: 메타데이터 필드 정보\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "b1fb20bf",
   "metadata": {},
   "outputs": [],
   "source": [
    "from langchain.retrievers.self_query.base import SelfQueryRetriever\n",
    "from langchain_openai import ChatOpenAI\n",
    "\n",
    "# LLM 정의\n",
    "llm = ChatOpenAI(model=\"gpt-4o-mini\", temperature=0)\n",
    "\n",
    "# SelfQueryRetriever 생성\n",
    "retriever = SelfQueryRetriever.from_llm(\n",
    "    llm=llm,\n",
    "    vectorstore=vectorstore,\n",
    "    document_contents=\"Brief summary of a cosmetic product\",\n",
    "    metadata_field_info=metadata_field_info,\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "427dfb18",
   "metadata": {},
   "source": [
    "## Query 테스트"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a4fa4817",
   "metadata": {},
   "source": [
    "필터를 걸 수 있는 질의를 입력하여 검색을 수행합니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "a59b1e5c",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Self-query 검색\n",
    "retriever.invoke(\"평점이 4.8 이상인 제품을 추천해주세요\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "0cf6368b",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Self-query 검색\n",
    "retriever.invoke(\"2023년에 출시된 상품을 추천해주세요\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "172ddf61",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Self-query 검색\n",
    "retriever.invoke(\"카테고리가 선케어인 상품을 추천해주세요\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a5cea901",
   "metadata": {},
   "source": [
    "복합 필터를 사용하여 검색을 수행할 수 있습니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "20f38f98",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Self-query 검색\n",
    "retriever.invoke(\n",
    "    \"카테고리가 메이크업인 상품 중에서 평점이 4.5 이상인 상품을 추천해주세요\"\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "2af3ea60",
   "metadata": {},
   "source": [
    "`k`는 가져올 문서의 수를 의미합니다.\n",
    "\n",
    "`SelfQueryRetriever`를 사용하여 `k`를 지정할 수도 있습니다. 이는 생성자에 `enable_limit=True`를 전달하여 수행할 수 있습니다.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "959cc5f4",
   "metadata": {},
   "outputs": [],
   "source": [
    "retriever = SelfQueryRetriever.from_llm(\n",
    "    llm=llm,\n",
    "    vectorstore=vectorstore,\n",
    "    document_contents=\"Brief summary of a cosmetic product\",\n",
    "    metadata_field_info=metadata_field_info,\n",
    "    enable_limit=True,  # 검색 결과 제한 기능을 활성화합니다.\n",
    "    search_kwargs={\"k\": 2},  # k 의 값을 2로 지정하여 검색 결과를 2개로 제한합니다.\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c86c0dfb",
   "metadata": {},
   "source": [
    "2023년도 출시된 상품은 3개가 있지만 \"k\" 값을 2로 지정하여 2개만 반환하도록 합니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "4628ffbc",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Self-query 검색\n",
    "retriever.invoke(\"2023년에 출시된 상품을 추천해주세요\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ed520315",
   "metadata": {},
   "source": [
    "하지만 코드로 명시적으로 `search_kwargs`를 지정하지 않고 query 에서 `1개, 2개` 등의 숫자를 사용하여 검색 결과를 제한할 수 있습니다.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "9b4cabe1",
   "metadata": {},
   "outputs": [],
   "source": [
    "retriever = SelfQueryRetriever.from_llm(\n",
    "    llm=llm,\n",
    "    vectorstore=vectorstore,\n",
    "    document_contents=\"Brief summary of a cosmetic product\",\n",
    "    metadata_field_info=metadata_field_info,\n",
    "    enable_limit=True,  # 검색 결과 제한 기능을 활성화합니다.\n",
    ")\n",
    "\n",
    "# Self-query 검색\n",
    "retriever.invoke(\"2023년에 출시된 상품 1개를 추천해주세요\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "9af3a1ab",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Self-query 검색\n",
    "retriever.invoke(\"2023년에 출시된 상품 2개를 추천해주세요\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "72a35c08",
   "metadata": {},
   "source": [
    "## 더 깊게 들어가기\n",
    "\n",
    "내부에서 어떤 일이 일어나는지 확인하고 더 많은 사용자 정의 제어를 하기 위해, 우리는 retriever를 처음부터 재구성할 수 있습니다.\n",
    "\n",
    "이 과정은 `query-construction chain` 을 생성하는 것부터 시작합니다.\n",
    "\n",
    "- [참고 튜토리얼](https://github.com/langchain-ai/langchain/blob/master/cookbook/self_query_hotel_search.ipynb) "
   ]
  },
  {
   "cell_type": "markdown",
   "id": "4a5e8059",
   "metadata": {},
   "source": [
    "### `query_constructor` chain 생성\n",
    "\n",
    "구조화된 쿼리를 생성하는 `query_constructor` chain 을 생성합니다."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "13881ef6",
   "metadata": {},
   "source": [
    "`get_query_constructor_prompt` 함수를 사용하여 쿼리 생성기 프롬프트를 가져옵니다.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "8e9b180d",
   "metadata": {},
   "outputs": [],
   "source": [
    "from langchain.chains.query_constructor.base import (\n",
    "    StructuredQueryOutputParser,\n",
    "    get_query_constructor_prompt,\n",
    ")\n",
    "\n",
    "# 문서 내용 설명과 메타데이터 필드 정보를 사용하여 쿼리 생성기 프롬프트를 가져옵니다.\n",
    "prompt = get_query_constructor_prompt(\n",
    "    \"Brief summary of a cosmetic product\",  # 문서 내용 설명\n",
    "    metadata_field_info,  # 메타데이터 필드 정보\n",
    ")\n",
    "\n",
    "# StructuredQueryOutputParser 를 생성\n",
    "output_parser = StructuredQueryOutputParser.from_components()\n",
    "\n",
    "# query_constructor chain 을 생성\n",
    "query_constructor = prompt | llm | output_parser"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "dd7e94a2",
   "metadata": {},
   "source": [
    "`prompt.format()` 메서드를 사용하여 `query` 매개변수에 \"dummy question\" 문자열을 전달하고, 그 결과를 출력하여 Prompt 내용을 확인해 보겠습니다.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "74bc86da",
   "metadata": {},
   "outputs": [],
   "source": [
    "# prompt 출력\n",
    "print(prompt.format(query=\"dummy question\"))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "5e9d0f92",
   "metadata": {},
   "source": [
    "`query_constructor.invoke()` 메서드를 호출하여 주어진 쿼리에 대한 처리를 수행합니다.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "bc33a7ac",
   "metadata": {},
   "outputs": [],
   "source": [
    "query_output = query_constructor.invoke(\n",
    "    {\n",
    "        # 쿼리 생성기를 호출하여 주어진 질문에 대한 쿼리를 생성합니다.\n",
    "        \"query\": \"2023년도에 출시한 상품 중 평점이 4.5 이상인 상품중에서 스킨케어 제품을 추천해주세요\"\n",
    "    }\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ffed6519",
   "metadata": {},
   "source": [
    "생성된 쿼리를 확인해 보겠습니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "c5d13f2f",
   "metadata": {},
   "outputs": [],
   "source": [
    "# 쿼리 출력\n",
    "query_output.filter.arguments"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "7f762ab4",
   "metadata": {},
   "source": [
    "Self-query retriever의 핵심 요소는 query constructor입니다. 훌륭한 검색 시스템을 만들기 위해서는 query constructor가 잘 작동하도록 해야 합니다.\n",
    "\n",
    "이를 위해서는 **프롬프트(Prompt), 프롬프트 내의 예시, 속성 설명 등을 조정** 해야 합니다."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "6422bc8b",
   "metadata": {},
   "source": [
    "### 구조화된 쿼리 변환기(Structured Query Translator)를 사용하여 구조화된 쿼리로 변환\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a88986cf",
   "metadata": {},
   "source": [
    "다음으로 중요한 요소는 structured query translator입니다. \n",
    "\n",
    "이는 일반적인 `StructuredQuery` 객체를 사용 중인 vector store의 구문에 맞는 메타데이터 필터로 변환하는 역할을 담당합니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "a4d228d3",
   "metadata": {},
   "outputs": [],
   "source": [
    "from langchain.retrievers.self_query.chroma import ChromaTranslator\n",
    "\n",
    "retriever = SelfQueryRetriever(\n",
    "    query_constructor=query_constructor,  # 이전에 생성한 query_constructor chain 을 지정\n",
    "    vectorstore=vectorstore,  # 벡터 저장소를 지정\n",
    "    structured_query_translator=ChromaTranslator(),  # 쿼리 변환기\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "941b5561",
   "metadata": {},
   "source": [
    "`retriever.invoke()` 메서드를 사용하여 주어진 질문에 대한 답변을 생성합니다.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "ad4b4199",
   "metadata": {},
   "outputs": [],
   "source": [
    "retriever.invoke(\n",
    "    # 질문\n",
    "    \"2023년도에 출시한 상품 중 평점이 4.5 이상인 상품중에서 스킨케어 제품을 추천해주세요\"\n",
    ")"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "py-test",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.11.9"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
